package jcu.salt.client;

import java.io.IOException;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Set;

import jcu.sal.common.CommandFactory;
import jcu.sal.common.Response;
import jcu.sal.common.StreamID;
import jcu.sal.common.CommandFactory.Command;
import jcu.sal.common.cml.CMLConstants;
import jcu.sal.common.cml.CMLDescription;
import jcu.sal.common.cml.ResponseType;
import jcu.sal.common.cml.StreamCallback;
import jcu.sal.common.exceptions.ConfigurationException;
import jcu.sal.common.exceptions.SensorControlException;
import jcu.sal.common.sml.SMLDescription;
import jcu.salt.Configuration;
import jcu.salt.Logger;
import jcu.salt.io.Sender;
import jcu.salt.io.Transmittable;

/**
 * maintains the connection for one specific sensor type/protocol to support independent
 * polling of the sensor values;
 * 
 * the interval used for polling is defined within the {@link Configuration};
 * 
 * data received from SAL is filled into a {@link Transmittable} object, enriched
 * with some metadata to be send to the {@link Sender}, which maintains the connection
 * to the data turbine for the specified sensor type/protocol
 * 
 * @author SAL-T team
 *
 */
public class SensorThread implements StreamCallback {

	private static final ResponseType RET_TYPE_BYTE_ARRAY = ResponseType.ByteArray; 

	// the sensor types
	public static final String PROTOCOL_VIDEO = "v4l";
	public static final String PROTOCOL_1WIRE = "1wire";
	public static final String PROTOCOL_SNMP = "EMS_SNMP";
	public static final String PROTOCOL_OSDATA = "osdata";
	
	// v4l protocol constants
	public static final String WIDTH_VALUE_NAME="width";
	public static final String HEIGHT_VALUE_NAME="height";
	public static final String CHANNEL_VALUE_NAME="channel";
	public static final String STANDARD_VALUE_NAME="standard";
	public static final String FORMAT_VALUE_NAME="format";
	public static final String QUALITY_VALUE_NAME="quality";

	private static final int DEFAULT_INTERVAL = 10000;

	/** the connection to the SAL agent */
	private final SaltClient client;
	
	/** the protocol this SensorThread is used for */
	private String protocolType;
	
	/** Map of stream IDs and return types (as strings) */
	private Hashtable<StreamID, String> streams;
	
	/** DT connection */
	private Sender sender;
	
	private Configuration config;

	/**
	 * Inits the thread
	 * @param protocolName type of sensor associated with this thread
	 * @param client established sal agent connection
	 */
	public SensorThread(String protocolName, final SaltClient client) {
		this.client = client;
		this.protocolType = protocolName;
		config = Configuration.getInstance();
		streams = new Hashtable<StreamID, String>();
	}

	/** stops this thread to poll the SAL */
	public synchronized void stop() {
		for(StreamID sid: new HashSet<StreamID>(streams.keySet()))
			client.stopStream(sid);
		sender.disconnect();
	}

	

	/**
	 * returns the std command for the current protocol
	 * 
	 * @return
	 */
	private int getCID() {
		return 100; 
	}
	
	/**
	 * This method creates a {@link Command} for sensor handled by this thread
	 * @param cml
	 * @return
	 * @throws ConfigurationException
	 */
	private Command getCommand(CMLDescription cml) throws ConfigurationException{
		CommandFactory c = new CommandFactory(cml, this);
		 if (protocolType.equals(PROTOCOL_VIDEO)) {
			c.addArgumentValueInt(WIDTH_VALUE_NAME, 640);
			c.addArgumentValueInt(HEIGHT_VALUE_NAME, 480);		
		}
		c.setInterval(getInterval());
		return c.getCommand();
	}
	
	public synchronized void createStreamForSensor(SMLDescription sml) {
		Command c = null;
		StreamID sid = null;
		CMLDescription cml = null;
		
		//get CML description for the sensor
		try {
			cml = client.getCML(sml).getDescription(getCID());
		} catch (Exception e1) {
			System.err.println("Error getting CML description for sensor "
					+sml.getID());
			e1.printStackTrace();
			return;
		} 
		
		//create the command
		try {
			c = getCommand(cml);
		} catch (Exception e) {
			System.err.println("Error creating command for sensor "
					+sml.getID());
			e.printStackTrace();
			return;
		}
		
		//start the stream
		try {
			Logger.log("Starting stream for sensor "+sml.getID());
			sid = client.startStream(c, sml.getID());
			Logger.log("stream started");
		} catch (Exception e) {
			System.err.println("Error starting stream for sensor "
					+sml.getID());
			e.printStackTrace();
			return;
		}
		
		//add stream id to map
		if (cml.getResponseType().equals(RET_TYPE_BYTE_ARRAY)) {
			streams.put(sid, CMLConstants.RET_TYPE_BYTE_ARRAY);
		} else { // send all others as string
			streams.put(sid, CMLConstants.RET_TYPE_STRING);
		}
	}

	/**
	 * main business logic : 
	 * 
	 * <li> fetch all sensors id's for the specified type/protocol </li> 
	 * <li> establish connection to data turbine using a {@link Sender} </li>
	 * <li> create stream from command 100 for each sensor
	 */
	public synchronized void start() {

		Logger.log("entered run for " + protocolType);
	
		//connect to DT
		Logger.log("connect to turbine for protocol " + protocolType);
		sender = new Sender(config
				.get(Configuration.DATATURBINE_IP), config
				.get(Configuration.SOURCE), config
				.get(Configuration.DEFAULTCHANNEL)
				+ "_" + protocolType.toLowerCase());
		sender.connect();

		//list sensors
		Set<SMLDescription> sensors = client.getSensorList(protocolType);

		Logger.log("we got " + sensors.size() + " sensors for protocol "
				+ protocolType);

		if (sensors != null && sensors.size() > 0) {
			for (SMLDescription smldesc : sensors) {
				try {
					createStreamForSensor(smldesc);
				} catch (Exception e) {
					System.err.println("Error starting stream for sensor"
							+ smldesc.getSID() + " of " + protocolType);
					e.printStackTrace();
				}
			}

		}			
	}

	/**
	 * reads out the given configuration to determine the adapted polling
	 * interval for the set protocol type
	 * 
	 * if the protocol type is unknown, then use the default interval
	 * 
	 * @see #DEFAULT_INTERVAL
	 * 
	 * @param config
	 * @return interval in milli seconds
	 */
	private int getInterval() {
		int interval = DEFAULT_INTERVAL;

		if (protocolType.equals(PROTOCOL_OSDATA)) {
			interval = Integer.parseInt(config.get(Configuration.TYPE_OS));
		} else if (protocolType.equals(PROTOCOL_SNMP)) {
			interval = Integer.parseInt(config.get(Configuration.TYPE_SNMP));
		} else if (protocolType.equals(PROTOCOL_1WIRE)) {
			interval = Integer.parseInt(config.get(Configuration.TYPE_1WIRE));
		} else if (protocolType.equals(PROTOCOL_VIDEO)) {
			interval = Integer.parseInt(config.get(Configuration.TYPE_VIDEO));
		}
		return interval;
	}

	/** getter for the associated protcol name */
	public String getProtocolName() {
		return protocolType;
	}

	@Override
	public void collect(Response r) throws IOException {
		Transmittable t = null;
		Logger.log("got callback for stream "+r.getStreamID());
		if(streams.get(r.getStreamID()).equals(CMLConstants.RET_TYPE_BYTE_ARRAY)){
			try {
				t = new Transmittable(
						client.getSalID(), 
						new Integer(r.getSID()).intValue(), 
						CMLConstants.RET_TYPE_BYTE_ARRAY,
						r.getBytes(),
						r.getTimeStamp());
			} catch (NumberFormatException e) {
				System.err.println("Error getting SensorID");
				e.printStackTrace();
				streams.remove(r.getStreamID());
				throw new IOException("Error getting sensor ID",e);
			} catch (SensorControlException e) {
				// SAL error , stream closed
				streams.remove(r.getStreamID());
				return;
			}

		} else {
			try {
				t = new Transmittable(
						client.getSalID(), 
						new Integer(r.getSID()).intValue(), 
						CMLConstants.RET_TYPE_BYTE_ARRAY,
						r.getString().getBytes(),
						r.getTimeStamp());
			} catch (NumberFormatException e) {
				System.err.println("Error getting SensorID");
				e.printStackTrace();
				streams.remove(r.getStreamID());
				throw new IOException("Error getting sensor ID",e);
			} catch (SensorControlException e) {
				// SAL error , stream closed
				streams.remove(r.getStreamID());
				return;
			}
		}
		
		//Logger.log("got callback for stream "+r.getStreamID());
		if(!sender.send(t))
			throw new IOException("Error forwarding data to DataTurbine");

		Logger.log("Recevied data for sensor "
				+ r.getSID());
	}

}
